Passed
Push — master ( bd926c...e51e0c )
by Barry
02:26
created

helpers.js ➔ _findCodeSpan   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
nc 4
nop 2
dl 0
loc 42
rs 8.5806
c 2
b 0
f 0

2 Functions

Rating   Name   Duplication   Size   Complexity  
A helpers.js ➔ ... ➔ _findCodeSpanEnd 0 11 2
A helpers.js ➔ ... ➔ _findCodeSpanStart 0 16 2
1
/** findMdpInsert and findCode functions use a similar layout to return the location and contents
2
  *   .start          => points at the character in the string why the other item starts (ie. comment or code block)
3
  *   .length         => is the overall length of the comment or code block.
4
  *   .internalStart  => points at the character in the string where the internal payload starts
5
  *   .internalLength => is the length of the internal payload
6
  *   .commandString  => is the command string found within the particular item
7
  *   .info           => is a structure containing further info about what was found
8
  * if start is returned as -1 then nothing was found
9
  *
10
  * The internalStart/internaLength defines the internal content which will be replaced. This does not include
11
  * leading and lagging CRLF/LF. So the replacement text is not required to have either leading or lagging line
12
  * endings. However, if the internalLength is negative this means that leading CRLF or LF must be added by the insertion
13
  * routine. The reason for this is that it allows insertions between code fences or mdpInsert pairs which have zero lines
14
  * between them.
15
  *
16
**/
17
18
export function findCode (txt, start) {
19
  /**
20
  * finds the next code in the string provided starting at position start
21
  * returns an object containing start, length, internalStart, internalLength
22
  *
23
  * there are three types of code insertion - code span (inline), fenced code and indented code
24
  *
25
  * eg. span (1 or more backticks):
26
  * some text ``echo myfile.txt`` more text
27
  *
28
  * eg. indented (4 or more indent spaces):
29
  * some text
30
  *     function test() {
31
  *       console.log('test')
32
  *     }
33
  * more text
34
  *
35
  * eg. fenced (3 or more backticks on a row on their own)
36
  * some text
37
  * ``` js
38
  * function test() {
39
  *   console.log('test')
40
  * }
41
  * ```
42
  * more text
43
  *
44
  **/
45
  let x = _findFencedCode(txt, start)
46
  let y = _findIndentedCode(txt, start)
47
  let z = _findCodeSpan(txt, start)
48
49
  return earlierOf(x, earlierOf(y, z))
50
}
51
52
export function findMdpCode (txt, start) {
53
  // finds the next location of mdpInsert in a code span within txt
54
  let posn = start
55
  let x
56
  while (true) {
57
    x = findCode(txt, posn)
58
    if (x.start === -1 || x.commandString.indexOf('mdpInsert ') !== -1) {
59
      getLineEnding(x, txt)
60
      return x
61
    } else {
62
      posn = x.start + x.length
63
    }
64
  }
65
}
66
67
export function findMdpInsert (txt, start) {
68
  let s = _findMdpStartUnfenced(txt, start)
69
  if (s.start === -1) { return s }
70
  let s1 = JSON.parse(JSON.stringify(s)) // create copy
71
  let depth = 1
72
  let e
73
  let posn = s1.internalStart
74
  while (depth !== 0) {
75
    e = _findMdpEndUnfenced(txt, s, posn)
76
    if (e.start === -1) {
77
      // we have not found any more ends so we need to return a fail
78
      return e
79
    }
80
    s1 = _findMdpStartUnfenced(txt, posn)
81
    if (s1.start !== -1) {
82
      // we have found another start pattern
83
      if (s1.start < (e.internalStart + e.internalLength)) {
84
        depth++
85
        posn = s1.internalStart
86
      } else {
87
        depth--
88
        posn = e.start + e.length
89
      }
90
    } else {
91
      depth--
92
      posn = e.start + e.length
93
    }
94
    if (depth > 5) { return {start: -1} }
95
  }
96
  getLineEnding(e, txt)
1 ignored issue
show
introduced by
The variable e does not seem to be initialized in case the while loop on line 74 is not entered. Are you sure the function getLineEnding handles undefined variables?
Loading history...
97
  return e
98
}
99
100
export function earlierOf (a, b) {
101
  // inspects the .start property of a and b and returns the one
102
  // with the lowest start position
103
  if (b.start !== -1 && (a.start === -1 || b.start < a.start)) {
104
    return b
105
  } else {
106
    return a
107
  }
108
}
109
110
function getLineEnding (s, txt) {
111
  // returns a basic structure with the type of line ending found within the text
112
  if (typeof s.info === 'undefined') { s.info = {} }
113
  if (txt.indexOf('\r\n') === -1) {
114
    s.info.endOfLine = '\n'
115
  } else {
116
    s.info.endOfLine = '\r\n'
117
  }
118
}
119
120
function _findMdpStartUnfenced (txt, start) {
121
  let lookFrom = start
122
  let m, c
123
  while (true) {
124
    m = _findMdpStart(txt, lookFrom)
125
    if (m.start === -1) { return m }
126
    c = findCode(txt, lookFrom)
127
    if (c.start === -1 || m.start < c.start || m.start > (c.start + c.length)) {
128
      // the mdp start we've found is not within a code fence
129
      break
130
    }
131
    // the mdp start we've found is within a code fence so find the next one
132
    lookFrom = c.start + c.length
133
  }
134
  return m
135
136
  function _findMdpStart (txt, start) {
137
    let regex = /(\r\n|\n|^)([ ]{0,3}\[>[^\r\n\t\0[\]]*\]: # (\([^\r\n\t\0]*\)|"[^\r\n\t\0]*"|'[^\r\n\t\0]*'))(\r\n|\n)/g
138
    regex.lastIndex = start
139
    let regexResult = regex.exec(txt)
140
    if (regexResult === null) { return {start: -1} }
141
    let r = {
142
      start: regexResult.index + regexResult[1].length,
143
      internalStart: regexResult.index + regexResult[0].length,
144
      commandString: regexResult[3].substring(1, regexResult[3].length - 1)
145
    }
146
    return r
147
  }
148
}
149
150
function _findMdpEndUnfenced (txt, opening, start) {
151
  let lookFrom = start
152
  let m, c
153
  while (true) {
154
    m = _findMdpEnd(txt, opening, lookFrom - 2)
155
    if (m.start === -1) { return m }
156
    c = findCode(txt, lookFrom)
157
    if (c.start === -1 || (m.internalStart + m.internalLength) < c.start || (m.internalStart + m.internalLength) > (c.start + c.length)) { break } // the mdp end we've found is not within a code fence
158
    // the mdp end we've found is within a code fence so find the next one
159
    lookFrom = c.start + c.length
160
  }
161
  return m
162
163
  function _findMdpEnd (txt, opening, start) {
164
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
165
    let regex = /(\r\n|\n)([ ]{0,3}\[<[^\r\n\t\0[\]]*\]: #)(\r\n|\n|$)/g
166
    regex.lastIndex = start
167
    let regexResult = regex.exec(txt)
168
    if (regexResult === null) { return {start: -1} }
169
    r.internalLength = regexResult.index - r.internalStart
170
    r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
171
    return r
172
  }
173
}
174
175
function _findCodeSpan (txt, start) {
176
  // finds an inline Code Span in the format: 'some text ``echo myfile.txt`` more text'
177
  // look for start
178
  let lookFrom = start
179
  while (true) {
180
    let s = _findCodeSpanStart(txt, lookFrom)
181
    if (s.start === -1) { return s }
182
    // look for end
183
    let e = _findCodeSpanEnd(txt, s)
184
    if (e.start !== -1) { return e }
185
    lookFrom = s.internalStart
186
  }
187
188
  function _findCodeSpanStart (txt, start) {
189
    let regex = /(^|[^`])(`+)[^`]/g
190
    // 1st capture group is the first (or no) character prior to the identifying `'s
191
    // 2nd group is the ` characters (however many there are)
192
    regex.lastIndex = start
193
    let regexResult = regex.exec(txt)
194
    if (regexResult === null) { return {start: -1} }
195
    let r = {
196
      start: regexResult.index + regexResult[1].length,
197
      internalStart: regexResult.index + regexResult[1].length + regexResult[2].length,
198
      info: {
199
        codeFence: regexResult[2]
200
      }
201
    }
202
    return r
203
  }
204
205
  function _findCodeSpanEnd (txt, opening) {
206
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
207
    let regex = RegExp('([^`])(' + r.info.codeFence + ')($|[^`])', 'g')
208
    regex.lastIndex = r.internalStart
209
    let regexResult = regex.exec(txt)
210
    if (regexResult === null) { return {start: -1} }
211
    r.internalLength = regexResult.index + regexResult[1].length - r.internalStart
212
    r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
213
    r.commandString = ''
214
    return r
215
  }
216
}
217
218
function _findIndentedCode (txt, start) {
219
  let regex = /((?:^|\r\n|\n)[ ]{4,}[^\r\n\0]*){1,}/g
220
  regex.lastIndex = start
221
  let regexResult = regex.exec(txt)
222
  if (regexResult === null) {
223
    return {start: -1}
224
  } else {
225
    return {
226
      start: regexResult.index,
227
      length: regexResult[0].length,
228
      internalStart: regexResult.index,
229
      internalLength: regexResult[0].length,
230
      info: {indent: regexResult[2]},
231
      commandString: ''
232
    }
233
  }
234
}
235
236
function _findFencedCode (txt, start) {
237
  // if the internalLength is returned as -1 this means that text cannot simply be inserted at the internalStart
238
  // location. Instead an additional preceding new line must be inserted along with the new text
239
  // another way to look at this is that the internal text is 1 character short
240
  // a value of -2 indicates a CRLF needs to be inserted
241
  let a = _findOpeningCodeFence(txt, start)
242
  if (a.start === -1) { return a }
243
  return _findClosingCodeFence(txt, a)
244
245
  function _findOpeningCodeFence (txt, start) {
246
    // returns the location and type of the next opening code fence
247
    let regex = /(^|\r\n|\n)([ ]{0,3}> |>|[ ]{0,0})(([ ]{0,3})([`]{3,}|[~]{3,})([^\n\r\0`]*))($|\r\n|\n)/g
248
    /** The regex groups are:
249
      * 0: the full match including any preamble block markup
250
      * 1: the leading new line character(s)
251
      * 2: the preamble consisting of block characters or nothing
252
      * 3: the full codeFence line without preamble
253
      * 4: any leading blank spaces at the start of the codeFence line
254
      * 5: the ` or ~ characters identifying the codeFence
255
      * 6: anything else on the line following the codeFence
256
      * 7: the final new line character(s)
257
    **/
258
    regex.lastIndex = start
259
    let regexResult = regex.exec(txt)
260
    if (regexResult === null) {
261
      return {start: -1}
262
    }
263
    let r = { start: regexResult.index + regexResult[1].length,
264
      info: {
265
        blockQuote: regexResult[2],
266
        spacesCount: regexResult[4].length,
267
        codeFence: regexResult[5]
268
      },
269
      commandString: regexResult[6].trim(),
270
      internalStart: regexResult.index + regexResult[0].length
271
    }
272
    return r
273
  }
274
275
  function _findClosingCodeFence (txt, opening) {
276
    // updates the passed result structure with the location and type of the next closing code fence
277
    // to match the opening cofeFence passed in
278
    let regex
279
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
280
    regex = RegExp('(^|\r\n|\n)([ ]{0,3}> |>|[ ]{0,0})[ ]{0,3}[' + r.info.codeFence[0] + ']{' + r.info.codeFence.length + ',}[ ]*($|\r\n|\n)', 'g')
281
    regex.lastIndex = r.internalStart - 2
282
    let regexResult = regex.exec(txt)
283
    if (opening.info.blockQuote.length !== 0) {
284
      // we are in a block quote so the codeFence will end at the earlier of the found regex OR end of the block quote
285
      let b = _findEndOfBlock(txt, r.internalStart)
286
      if (b !== -1 && (regexResult === null || b < (regexResult.index + regexResult[1].length))) {
287
        // the block end dictates the code block end
288
        r.internalLength = b - r.internalStart
289
        r.length = b - r.start
290
        return r
291
      }
292
    }
293
    if (regexResult === null) {
294
      r.internalLength = txt.length - r.internalStart
295
      r.length = txt.length - r.start
296
    } else {
297
      r.internalLength = regexResult.index - r.internalStart
298
      r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
299
    }
300
    return r
301
  }
302
303
  function _findEndOfBlock (txt, start) {
304
    // finds the first line which is not marked as block
305
    let regex = /(\r\n|\n)(?!([ ]{0,3}> |>))[^>\r\n]*/g
306
    regex.lastIndex = start
307
    let regexResult = regex.exec(txt)
308
    if (regexResult === null) {
309
      return -1
310
    } else {
311
      return regexResult.index
312
    }
313
  }
314
}
315